package com.spark.sniffer.plugin.plugins.interceptor;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.spark.sniffer.constant.PassThroughEnum;
import com.spark.sniffer.constant.TracingDataConstant;
import com.spark.sniffer.logging.api.Logger;
import com.spark.sniffer.logging.api.LoggerFactory;
import com.spark.sniffer.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
import com.spark.sniffer.plugin.interceptor.enhance.MethodInterceptResult;
import com.spark.sniffer.tracing.Span;
import com.spark.sniffer.tracing.TraceId;
import com.spark.sniffer.tracing.TracingContext;
import com.spark.sniffer.util.ReflectUtil;
import com.spark.sniffer.util.ReflectUtil.WrapperMethod;
import com.spark.sniffer.util.StringUtil;

public class DispatcherServletInterceptor implements InstanceMethodsAroundInterceptor {
    private static final Logger log = LoggerFactory.getLogger(DispatcherServletInterceptor.class);

    private static final List<String> IGNORES_SUFFIX =
        new ArrayList<>(Arrays.asList(".ico", ".jpg", ".jpeg", ".bmp", ".gif"));
    private static final List<String> IGNORES_PREFIX = new ArrayList<>(Arrays.asList("/actuator/"));
    private static final List<String> IGNORES_URI = new ArrayList<>(
        Arrays.asList("/", "/favicon.ico", "/error", "/que/k8s/health/check", "/helthy/sys/monitor/publish/query"));

    static {
        String uriString = System.getenv("sniffer_ignore_uri");
        if (StringUtil.isNotEmpty(uriString)) {
            IGNORES_URI.addAll(Arrays.asList(uriString.split(",")));
            log.info("Load ignore uri from env: {}", uriString);
        }

        String prefixString = System.getenv("sniffer_ignore_prefix");
        if (StringUtil.isNotEmpty(prefixString)) {
            IGNORES_PREFIX.addAll(Arrays.asList(prefixString.split(",")));
            log.info("Load ignore prefix from env: {}", prefixString);
        }

        String suffixString = System.getenv("sniffer_ignore_suffix");
        if (StringUtil.isNotEmpty(suffixString)) {
            IGNORES_SUFFIX.addAll(Arrays.asList(suffixString.split(",")));
            log.info("Load ignore suffix from env: {}", suffixString);
        }
    }

    @Override
    public void beforeMethod(Object instance, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
        MethodInterceptResult result) throws Throwable {
        String uri = ReflectUtil.getMethod(allArguments[0], "getRequestURI").invoke();
        if (isIgnoreUri(uri)) {
            return;
        }

        TracingContext.getContext().entrance(Span.Type.Request, uri);

        // 如果之前不经过Shiro Plugin处理，则进行下列处理，尝试从request.header获取traceid等数据，设置到context中，以便在该线程后续http或rpc操作中进行分布式透传
        // 特别的，traceid如果header不存在，则新生成之
        if (TracingContext.getContext().getTraceId() == null) {
            getAndPutPassThroughsToContext(allArguments);
        }
    }

    @Override
    public Object afterMethod(Object instance, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
        Object ret) throws Throwable {
        try {
            // 获得http状态码
            int status = ReflectUtil.getMethod(allArguments[1], "getStatus").invoke();
            Span span = TracingContext.getContext().stopEntrance(status);

            // 从request.getAttribute("uid")获取用户信息打印到输出日志中
            if (span != null) {
                String uid = ReflectUtil.getMethod(allArguments[0], "getAttribute", String.class).invoke("uid");
                printCallStack(uid == null ? "" : uid);
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            // DO remember this!!!
            TracingContext.removeContext();
        }

        return ret;
    }

    @Override
    public void handleMethodException(Object instance, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
        Throwable t) {
        // TracingContext.getContext().stopEntrance(Span.ERR_CODE);
    }

    private boolean isIgnoreUri(String uri) {
        if (uri == null || uri.trim().isEmpty()) {
            return true;
        }

        return IGNORES_URI.parallelStream().anyMatch(x -> x.equals(uri))
            || IGNORES_PREFIX.parallelStream().anyMatch(x -> uri.startsWith(x))
            || IGNORES_SUFFIX.parallelStream().anyMatch(x -> uri.endsWith(x));
    }

    /**
     * @param allArguments
     */
    private void getAndPutPassThroughsToContext(Object[] allArguments) {
        // 反射获取getHeader()方法
        WrapperMethod getHeaderMethod = ReflectUtil.getMethod(allArguments[0], "getHeader", String.class);

        // 从header中获取透传数据设置到上下文中
        for (PassThroughEnum passThrough : PassThroughEnum.values()) {
            String key = passThrough.getValue();
            String value = getHeaderMethod.invoke(key);

            // traceid特别处理
            if (TraceId.KEY.equals(key)) {
                if (value == null) {
                    value = TraceId.generate();
                }
                TraceId.putToContext(value);

                // 将traceid设置到response.header[traceid]中，以便回显在调用端
                ReflectUtil.getMethod(allArguments[1], "setHeader", String.class, String.class).invoke(TraceId.KEY,
                    value);
            }

            if (value == null || value.trim().equals("")) {
                continue;
            }

            TracingContext.getContext().putPassThrough(key, value);
            log.debug("put passThrough to context: {}={}", key, value);
        }
    }

    private void printCallStack(String uid) {
        Span entranceSpan = TracingContext.getContext().getEntrance();
        if (entranceSpan == null) {
            return;
        }

        // status是http status，code是api响应码
        log.info("tid={}, uri={}, uid={}, status={}, code={}, ts={}{}, trace=[{}], pass=[{}]",
            TracingContext.getContext().getTraceId(), entranceSpan.getName(), uid, entranceSpan.getCode(),
            TracingContext.getContext().getData(TracingDataConstant.ERRCODE, entranceSpan.getCode()),
            entranceSpan.getDuration(), entranceSpan.getDuration() > 1000 ? ", slow=y" : "",
            TracingContext.getContext().callstack2string(), TracingContext.getContext().passThrough2string());
    }

}
